// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;

namespace SnapObjects.Data
{
    public abstract class SqlTokenizerBase : IDisposable
    {
        protected readonly StringReader _sourceReader;

        protected static HashSet<char> _delimiters = new HashSet<char>(
            new char[] { ' ', '\t', '\r', '\n', '.', '\'', '\"', ',', ';', '(', ')', '[', ']', '{', '}' });

        protected static HashSet<char> _operators = new HashSet<char>(
            new char[] { '=', '~', '-', '+', '*', '/', '<', '>', '!', '&', '|', '^', '%' });

        private readonly bool _skipBlank;

        public SqlTokenizerBase(string sqlText) : this(sqlText, false)
        {
        }

        public SqlTokenizerBase(string sqlText, bool skipBlank)
        {
            _skipBlank = skipBlank;
            _sourceReader = new StringReader(sqlText);

            this.SqlText = sqlText;
            this.Buffer = new StringBuilder();
        }

        public string SqlText { get; }

        protected bool IsEnd()
        {
            return _sourceReader.Peek() == -1;
        }

        protected bool IsDelimiter(char source)
        {
            return _delimiters.Contains(source) || _operators.Contains(source);
        }

        protected bool IsCurrencySymbol(char source)
        {
            return CharUnicodeInfo.GetUnicodeCategory(source) == UnicodeCategory.CurrencySymbol;
        }

        protected bool IsDigit(char source)
        {
            return Char.IsDigit(source)
                || source == '+'
                || source == '-'
                || source == '.';
        }

        protected StringBuilder Buffer { get; }

        protected bool HasContent => this.Buffer.Length > 0;

        protected char CurrentCharacter
        {
            get
            {
                var peek = _sourceReader.Peek();
                return peek == -1 ? '\0' : (char)peek;
            }
        }

        protected bool IsBlankCharacter(char blank)
        {
            return blank == ' ' || blank == '\r' || blank == '\n' || blank == '\t';
        }

        protected string NextToken()
        {
            this.Buffer.Clear();

            // column name, e.g. "price"
            if (this.CurrentCharacter == '\"')
            {
                this.ReadCurrent();
                this.ReadUntil('\"');
                this.ReadCurrent();
            }
            // maybe string, e.g. N'test string.'
            else if (this.CurrentCharacter == 'N')
            {
                this.ReadCurrent();

                // string
                if (this.CurrentCharacter == '\'')
                {
                    this.ReadCurrent();
                    this.ReadUntil('\'');
                    this.ReadCurrent();
                }
                else
                {
                    this.ReadUntilDelimiter();
                }
            }
            else if (this.CurrentCharacter == '[')
            {
                this.ReadCurrent();
                this.ReadUntil(']');
                this.ReadCurrent();
            }
            // string, e.g. 'test string.'
            else if (this.CurrentCharacter == '\'')
            {
                this.ReadCurrent();
                this.ReadUntil('\'');
                this.ReadCurrent();
            }
            else if (this.CurrentCharacter == '/')
            {
                this.ReadCurrent();

                if (this.CurrentCharacter == '*')
                {
                    while (this.CurrentCharacter != '/')
                    {
                        this.ReadUntil('*');
                        this.ReadCurrent();
                    }

                    this.ReadCurrent();
                }
                else if (this.CurrentCharacter == '/')
                {
                    this.ReadUntil('\n');
                    this.ReadCurrent();
                }
            }
            else if (this.CurrentCharacter == '-')
            {
                this.ReadCurrent();

                if (this.CurrentCharacter == '-')
                {
                    this.ReadUntil('\n');
                    this.ReadCurrent();
                }
                else
                {
                    this.ReadNumber();
                }
            }
            else if (this.IsBlankCharacter(this.CurrentCharacter))
            {
                this.ReadBlank(_skipBlank);
            }
            else if (this.IsDelimiter(this.CurrentCharacter))
            {
                this.ReadCurrent();
            }
            else if (this.IsCurrencySymbol(this.CurrentCharacter))
            {
                this.ReadCurrent();
                this.ReadNumber();
            }
            else if (this.IsDigit(this.CurrentCharacter))
            {
                this.ReadNumber();
            }
            else
            {
                this.ReadUntilDelimiter();
            }

            return this.Buffer.ToString();
        }

        private void ReadCurrent()
        {
            var current = (char)_sourceReader.Read();
            this.Buffer.Append(current);
        }

        private void ReadBlank(bool skip)
        {
            while (!this.IsEnd())
            {
                if (this.IsBlankCharacter(this.CurrentCharacter))
                {
                    var current = (char)_sourceReader.Read();

                    if (!skip)
                    {
                        this.Buffer.Append(current);
                    }
                }
                else
                {
                    break;
                }
            }
        }

        private void ReadNumber()
        {
            var index = 0;
            var hasDot = false;
            var hasHex = false;

            while (!this.IsEnd())
            {
                if ((index == 0 && this.IsDigit(this.CurrentCharacter)) ||
                    Char.IsDigit(this.CurrentCharacter) ||
                    (!hasDot && this.CurrentCharacter == '.') ||
                    (!hasHex && (this.CurrentCharacter == 'x' || this.CurrentCharacter == 'X')))
                {
                    var current = (char)_sourceReader.Read();
                    this.Buffer.Append(current);

                    if (current == '.')
                    {
                        hasDot = true;
                    }

                    if (current == 'x' || current == 'X')
                    {
                        hasHex = true;
                    }
                }
                else
                {
                    break;
                }

                index++;
            }
        }

        private void ReadUntil(char target)
        {
            while (!this.IsEnd())
            {
                if (this.CurrentCharacter != target)
                {
                    var current = (char)_sourceReader.Read();
                    this.Buffer.Append(current);
                }
                else
                {
                    break;
                }
            }
        }

        private void ReadUntilDelimiter()
        {
            while (!this.IsEnd())
            {
                if (!this.IsDelimiter(this.CurrentCharacter))
                {
                    var current = (char)_sourceReader.Read();
                    this.Buffer.Append(current);
                }
                else
                {
                    break;
                }
            }
        }

        protected IList<string> GetTokens()
        {
            var tokens = new List<string>();

            while (!this.IsEnd())
            {
                var token = this.NextToken();

                if (_skipBlank && String.IsNullOrWhiteSpace(token))
                {
                    continue;
                }

                tokens.Add(token);
            }

            return tokens;
        }

        public void Dispose()
        {
            _sourceReader.Dispose();
        }
    }
}
